﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.IO.Compression;
using System.Reflection;
using System.Text;
using Beaker.Injection;
using OpenCube.IO;

namespace OpenCube.Nbt
{
	/// <summary>
	/// Reader for reading NBT files from a stream.
	/// </summary>
	[CLSCompliant(false)]
	public sealed class NbtDeserializer
		: INbtDeserializer
	{
		private readonly IBinaryReaderFactory _readerFactory;
		private readonly IFactory<INbtDictionary> _dictionaryFactory;

		/// <summary>
		/// Create a new NBT Reader.
		/// </summary>
		/// <param name="readerFactory">The factory for creating binary readers.</param>
		/// <param name="dictionaryFactory">The factory for creating NBT Dictionaries.</param>
		public NbtDeserializer(IBinaryReaderFactory readerFactory, IFactory<INbtDictionary> dictionaryFactory)
		{
			Contract.Requires<ArgumentNullException>(readerFactory != null, "A factory for creating binary readers is required.");
			Contract.Requires<ArgumentNullException>(dictionaryFactory != null, "A factory for creating NBT Dictionaries is required.");

			_readerFactory = readerFactory;
			_dictionaryFactory = dictionaryFactory;
		}

		/// <summary>
		/// Reads a tag from the nbt stream.
		/// </summary>
		/// <param name="reader">The binary reader to read the NBT tag from</param>
		/// <returns>The read NBT tag.</returns>
		/// <exception cref="NbtFileFormatException">Unknown TAG value</exception>
		private static NbtTag ReadTag(IBinaryReader reader)
		{
			Contract.Requires<ArgumentNullException>(reader != null, "A reader is required");
			Contract.Ensures(Enum.IsDefined(typeof(NbtTag), Contract.Result<NbtTag>()));

			// get tag byte
			var tag = (NbtTag)reader.ReadByte();

			// verifiy if it is in range
			if (!Enum.IsDefined(typeof(NbtTag), tag))
				throw new NbtFileFormatException("Unknown TAG value. The value " + (int)tag + " does not map to a known NBT tag.");

			return tag;
		}

		private static readonly UTF8Encoding Utf8 = new UTF8Encoding(false, true); // utf8 encoder that THROWS on invalid chars

		/// <summary>
		/// Reads a string from the NBT stream.
		/// </summary>
		/// <param name="reader">The binary reader to read the NBT string data from</param>
		/// <returns></returns>
		private static string ReadString(IBinaryReader reader)
		{
			Contract.Requires<ArgumentNullException>(reader != null, "A reader is required");
			Contract.Ensures(Contract.Result<string>() != null);

			ushort length = reader.ReadUInt16();
			if (length == 0)
				return String.Empty;

			byte[] bytes = reader.ReadBytes(length);

			try
			{
				return Utf8.GetString(bytes, 0, bytes.Length);
			}
			catch (DecoderFallbackException x)
			{
				throw new NbtFileFormatException("Not a valid UTF-8 string", x);
			}
		}

		/// <summary>
		/// Reads A node from the stream.
		/// </summary>
		/// <param name="reader">The binary reader to read the NBT string data from</param>
		/// <returns>A <see cref="KeyValuePair{TString,TObject}"/> containing the Name (Key) and the Value. or <c>null</c> when no more nodes available (TAG_End).</returns>
		private KeyValuePair<string, object>? ReadNode(IBinaryReader reader)
		{
			Contract.Requires<ArgumentNullException>(reader != null, "A reader is required");
			Contract.Ensures(Contract.Result<KeyValuePair<string, object>?>() == null || Contract.Result<KeyValuePair<string, object>?>().Value.Key != null);

			NbtTag tag = ReadTag(reader);

			// if tag is of type end, we are done now
			if (tag == NbtTag.End)
				return null;

			// otherwise a name (key) MUST follow
			string name = ReadString(reader);

			// and then the payload (value)
			object value = ReadData(reader, tag);

			return new KeyValuePair<string, object>(name, value);
		}

		/// <summary>
		/// Reads the data for the compound node.
		/// </summary>
		/// <param name="reader">The binary reader to read the NBT compount node data from</param>
		/// <returns>A dictionary of KeyValue pairs holding the data from the NBT stream.</returns>
		private INbtDictionary ReadCompound(IBinaryReader reader)
		{
			Contract.Ensures(Contract.Result<INbtDictionary>() != null);

			INbtDictionary dict = _dictionaryFactory.Create();

			// read until no more data in compound
			while (true)
			{
				// read a single node
				KeyValuePair<string, object>? node = ReadNode(reader);
				if (!node.HasValue)
					break;

				// add node to dictionary
				dict.Add(node.Value.Key, node.Value.Value);
			}

			return dict;
		}


		/// <summary>
		/// Reads the data for the list node
		/// </summary>
		/// <param name="reader">The binary reader to read the NBT list data from</param>
		/// <returns>The list node read from the stream.</returns>
		private IEnumerable ReadList(IBinaryReader reader)
		{
			Contract.Ensures(Contract.Result<IEnumerable>() != null);

			// read tag of items in list
			NbtTag itemsType = ReadTag(reader);
			// read number of items in list
			int count = reader.ReadInt32();

			if (count == 0 && itemsType == NbtTag.End)
				return new List<object>(); // unknown type, so return object list;

			// determine the system type the tag should map to
			Type systemItemsType = itemsType.GetSystemType();

			IList values;
			if (!systemItemsType.GetTypeInfo().IsGenericTypeDefinition)
				// actual type, so create list of actual type
				values = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(systemItemsType));
			else
				values = new List<object>();

			// read the given number of values from the stream
			for (int i = 0; i < count; i++)
			{
				// read value of given type from stream
				object value = ReadData(reader, itemsType);
				// add value to list
				values.Add(value);
			}

			// return list of all the read values
			return values;
		}

		/// <summary>
		/// Reads data of the give type.
		/// </summary>
		/// <param name="reader">The binary reader to read the NBT data from</param>
		/// <param name="type">The type of the data to read.</param>
		/// <returns>An object containing the value of the requested type.</returns>
		private object ReadData(IBinaryReader reader, NbtTag type)
		{
			Contract.Requires<ArgumentOutOfRangeException>(Enum.IsDefined(typeof(NbtTag), type));
			Contract.Ensures(Contract.Result<object>() != null);

			switch (type)
			{
				case NbtTag.Byte:
					return reader.ReadSByte(); // minecraft uses Signed bytes (weird... is this normal for Java apps?)

				case NbtTag.Short:
					return reader.ReadInt16();

				case NbtTag.Int:
					return reader.ReadInt32();

				case NbtTag.Long:
					return reader.ReadInt64();

				case NbtTag.Float:
					return reader.ReadSingle();

				case NbtTag.Double:
					return reader.ReadDouble();

				case NbtTag.ByteArray:
					int length = reader.ReadInt32();
					return reader.ReadSBytes(length);

				case NbtTag.String:
					return ReadString(reader);

				case NbtTag.List:
					return ReadList(reader);

				case NbtTag.Compound:
					return ReadCompound(reader);

				case NbtTag.IntArray:
					int count = reader.ReadInt32();
					var intList = new List<int>();
					for (int i = 0; i < count; i++)
						intList.Add(reader.ReadInt32());
					return intList.ToArray();
			}

			throw new NotImplementedException("Encountered an tag that cannot be handled");
		}

		/// <summary>
		/// Deserialize given stream as a <see cref="INbtDictionary"/>.
		/// </summary>
		/// <param name="stream">The stream to deserialize from.</param>
		/// <returns>The dictionary holding the deserialized data.</returns>
		public INbtDictionary Deserialize(Stream stream)
		{
			IBinaryReader reader = CreateReader(stream);

			NbtTag tag = ReadTag(reader);
			if (tag != NbtTag.Compound)
				throw new NbtFileFormatException("First tag MUST be a compound tag");

			// string that is read, is ignored (for now)
			ReadString(reader);

			return ReadCompound(reader);
		}

		/// <summary>
		/// Create a reader for the NBT Stream
		/// </summary>
		/// <param name="stream">The stream to create a reader for.</param>
		/// <returns>The reader for the NBT Stream.</returns>
		private IBinaryReader CreateReader(Stream stream)
		{
			Contract.Requires<ArgumentNullException>(stream != null, "A stream is required.");
			Contract.Requires<InvalidOperationException>(stream.CanRead, "The stream should be readable.");

			// we can only test for gzip compression when the stream is seekable
			if (stream.CanSeek)
			{
				// remember position of stream
				long originalStartPosition = stream.Position;

				// get 2 bytes so we can check if it starts with the magic gzip header
				var buffer = new byte[2];
				stream.Read(buffer, 0, 2);
				// ignore if we read less as 2 bytes, 
				// exception will probably be raised later while reading uncompressed stream
				// but not enough data for now to determine the correct error

				// move position back
				stream.Position = originalStartPosition;

				// check for the magic header, if found, use a gzip decompression stream
				if (buffer[0] == 0x1f && buffer[1] == 0x8b)
					stream = new GZipStream((Stream)stream, CompressionMode.Decompress);
			}

			// create a reader for big endian data
			return _readerFactory.CreateBinaryReader(stream, Endianness.BigEndian);
		}
	}
}